﻿namespace UDKDevKitVSXDebugEngine
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Text.RegularExpressions;
    using Microsoft.VisualStudio.Debugger.Interop;
    using UDKDevKitVSX.Concrete;
    using UDKDevKitVSXDebugEngine.Definitions;
    using UDKDevKitVSXDebugEngine.Exceptions;
    using UDKDevKitVSXDebugEngine.Utils;

    // This class represents a pending breakpoint which is an abstract representation of a breakpoint before it is bound.
    // When a user creates a new breakpoint, the pending breakpoint is created and is later bound. The bound breakpoints
    // become children of the pending breakpoint.
    internal class PendingBreakpoint : IDebugPendingBreakpoint2
    {
        // The breakpoint request that resulted in this pending breakpoint being created.

        private List<BoundBreakpoint> boundBreakpoints;
        private object boundBreakpointsLock = new object();

        private BP_CONDITION bpCondition;
        private BP_PASSCOUNT bpPasscountStyle;
        private BreakpointManager breakpointManager;
        private IDebugBreakpointRequest2 breakpointRequest;
        private BP_REQUEST_INFO breakpointRequestInfo;
        private bool deleted;
        private bool enabled;
        private Engine engine;

        public PendingBreakpoint(IDebugBreakpointRequest2 breakpointRequest, Engine engine, BreakpointManager breakpointManager)
        {
            if (engine == null)
            {
                Debug.Fail("ArgumentNullException - engine");
                throw new ArgumentNullException("engine");
            }

            if (breakpointRequest == null)
            {
                Debug.Fail("ArgumentNullException - breakpointRequest");
                throw new ArgumentNullException("breakpointRequest");
            }

            this.breakpointRequest = breakpointRequest;
            BP_REQUEST_INFO[] requestInfo = new BP_REQUEST_INFO[1];
            Core.CheckOk(this.breakpointRequest.GetRequestInfo(enum_BPREQI_FIELDS.BPREQI_ALLFIELDS, requestInfo));
            this.breakpointRequestInfo = requestInfo[0];

            this.bpCondition = this.breakpointRequestInfo.bpCondition;
            this.bpPasscountStyle = this.breakpointRequestInfo.bpPassCount;

            this.engine = engine;
            this.breakpointManager = breakpointManager;
            this.boundBreakpoints = new List<BoundBreakpoint>();

            this.enabled = true;
            this.deleted = false;
        }

        // Remove all of the bound breakpoints for this pending breakpoint

        #region Public Methods

        internal IList<BoundBreakpoint> GetBoundBreakpointsBelongTo(string className, uint lineCount)
        {
            lock (boundBreakpointsLock)
            {
                var boundBreakpointToRet = new List<BoundBreakpoint>();

                foreach (var boundBreakpoint in this.boundBreakpoints)
                {
                    if (boundBreakpoint.BelongTo(className, lineCount))
                    {
                        boundBreakpointToRet.Add(boundBreakpoint);
                    }
                }

                return boundBreakpointToRet;
            }
        }

        public BoundBreakpoint CreateBoundBreakpoints(Engine engine)
        {
            var boundBreakpoint =
                new BoundBreakpoint(
                    this.engine, this, new BreakpointResolution(this.engine, this.GetDocumentContext()));

            var debugBoundBreakpoint2 = boundBreakpoint as IDebugBoundBreakpoint2;
            debugBoundBreakpoint2.SetCondition(this.bpCondition);
            debugBoundBreakpoint2.SetPassCount(this.bpPasscountStyle);

            // add to array
            lock (this.boundBreakpointsLock)
            {
                this.boundBreakpoints.Add(boundBreakpoint);
            }

            return boundBreakpoint;
        }

        public void ClearBoundBreakpoints()
        {
            Debug.WriteLine("PendingBreakpoint ClearBoundBreakpoints");

            lock (this.boundBreakpointsLock)
            {
                for (int i = this.boundBreakpoints.Count - 1; i >= 0; i--)
                {
                    ((IDebugBoundBreakpoint2)this.boundBreakpoints[i]).Delete();
                }
            }
        }

        public DocumentContext GetDocumentContext()
        {
            switch ((enum_BP_LOCATION_TYPE)this.breakpointRequestInfo.bpLocation.bpLocationType)
            {
                case enum_BP_LOCATION_TYPE.BPLT_CODE_FILE_LINE:
                    string documentName;
                    TEXT_POSITION[] startPosition;
                    TEXT_POSITION[] endPosition;
                    GetStartPosition(out documentName, out startPosition, out endPosition);

                    return
                        new DocumentContext(
                            DocumentContext.GetClassNameFromFilePath(documentName),
                            documentName,
                            startPosition[0],
                            endPosition[0]);

                    break;

                default:
                    return null;
            }
        }

        // Called by bound breakpoints when they are being deleted.
        public void OnBoundBreakpointDeleted(BoundBreakpoint boundBreakpoint)
        {
            Debug.WriteLine("PendingBreakpoint OnBoundBreakpointDeleted");

            lock (this.boundBreakpointsLock)
            {
                this.boundBreakpoints.Remove(boundBreakpoint);
            }

            this.RemoveBreakpointInUDK();
        }

        #endregion

        // Binds this pending breakpoint to one or more code locations.

        #region Private Methods

        int IDebugPendingBreakpoint2.Bind()
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 Bind");

            try
            {
                if (this.CanBind())
                {
                    this.SetBreakpointInUDK();

                    return Constants.S_OK;
                }
                else
                {
                    // The breakpoint could not be bound. This may occur for many reasons such as an invalid location, an invalid expression, etc...
                    // The sample engine does not support this, but a real world engine will want to send an instance of IDebugBreakpointErrorEvent2 to the
                    // UI and return a valid instance of IDebugErrorBreakpoint2 from IDebugPendingBreakpoint2::EnumErrorBreakpoints. The debugger will then
                    // display information about why the breakpoint did not bind to the user.
                    return Constants.S_FALSE;
                }
            }
            catch (ComponentException e)
            {
                return e.HResult;
            }
            catch (Exception e)
            {
                return Core.UnexpectedException(e);
            }
        }

        private bool CanBind()
        {
            Debug.Write("PendingBreakpoint Bind");

            // The sample engine only supports breakpoints on a file and line number. No other types of breakpoints are supported.
            if (this.deleted
                || (this.breakpointRequestInfo.bpLocation.bpLocationType != (uint)enum_BP_LOCATION_TYPE.BPLT_CODE_FILE_LINE
                    && this.breakpointRequestInfo.bpLocation.bpLocationType != (uint)enum_BP_LOCATION_TYPE.BPLT_CODE_FUNC_OFFSET))
            {
                Debug.WriteLine(" false");

                return false;
            }

            Debug.WriteLine(" true");

            return true;
        }

        // Determines whether this pending breakpoint can bind to a code location.
        int IDebugPendingBreakpoint2.CanBind(out IEnumDebugErrorBreakpoints2 ppErrorEnum)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 CanBind");

            ppErrorEnum = null;

            if (!this.CanBind())
            {
                // Called to determine if a pending breakpoint can be bound. 
                // The breakpoint may not be bound for many reasons such as an invalid location, an invalid expression, etc...
                // The sample engine does not support this, but a real world engine will want to return a valid enumeration of IDebugErrorBreakpoint2.
                // The debugger will then display information about why the breakpoint did not bind to the user.
                ppErrorEnum = null;
                return Constants.S_FALSE;
            }

            return Constants.S_OK;
        }

        // Deletes this pending breakpoint and all breakpoints bound from it.
        int IDebugPendingBreakpoint2.Delete()
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 Delete");

            lock (this.boundBreakpointsLock)
            {
                for (int i = this.boundBreakpoints.Count - 1; i >= 0; i--)
                {
                    ((IDebugBoundBreakpoint2)this.boundBreakpoints[i]).Delete();
                }
            }

            return Constants.S_OK;
        }

        // Toggles the enabled state of this pending breakpoint.
        int IDebugPendingBreakpoint2.Enable(int fEnable)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 Enable");

            lock (this.boundBreakpointsLock)
            {
                this.enabled = fEnable == 0 ? false : true;

                foreach (var boundBreakpoint in this.boundBreakpoints)
                {
                    ((IDebugBoundBreakpoint2)boundBreakpoint).Enable(fEnable);
                }
            }

            Debug.WriteLine(fEnable == 0 ? "IDebugPendingBreakpoint2 Disabled" : "IDebugPendingBreakpoint2 Enabled");

            return Constants.S_OK;
        }

        // Enumerates all breakpoints bound from this pending breakpoint
        int IDebugPendingBreakpoint2.EnumBoundBreakpoints(out IEnumDebugBoundBreakpoints2 ppEnum)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 EnumBoundBreakpoints");

            lock (this.boundBreakpointsLock)
            {
                IDebugBoundBreakpoint2[] boundBreakpointsArray = this.boundBreakpoints.ToArray();
                ppEnum = new AD7BoundBreakpointsEnum(boundBreakpointsArray);
            }

            return Constants.S_OK;
        }

        // Enumerates all error breakpoints that resulted from this pending breakpoint.
        int IDebugPendingBreakpoint2.EnumErrorBreakpoints(
            enum_BP_ERROR_TYPE bpErrorType, out IEnumDebugErrorBreakpoints2 ppEnum)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 EnumErrorBreakpoints");

            // Called when a pending breakpoint could not be bound. This may occur for many reasons such as an invalid location, an invalid expression, etc...
            // The sample engine does not support this, but a real world engine will want to send an instance of IDebugBreakpointErrorEvent2 to the
            // UI and return a valid enumeration of IDebugErrorBreakpoint2 from IDebugPendingBreakpoint2::EnumErrorBreakpoints. The debugger will then
            // display information about why the breakpoint did not bind to the user.
            ppEnum = null;
            return Constants.E_NOTIMPL;
        }

        // Gets the breakpoint request that was used to create this pending breakpoint
        int IDebugPendingBreakpoint2.GetBreakpointRequest(out IDebugBreakpointRequest2 ppBPRequest)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 GetBreakpointRequest");

            ppBPRequest = this.breakpointRequest;
            return Constants.S_OK;
        }

        // Gets the state of this pending breakpoint.
        int IDebugPendingBreakpoint2.GetState(PENDING_BP_STATE_INFO[] pState)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 GetState");

            if (this.deleted)
            {
                pState[0].state = (enum_PENDING_BP_STATE)enum_BP_STATE.BPS_DELETED;
            }
            else if (this.enabled)
            {
                pState[0].state = (enum_PENDING_BP_STATE)enum_BP_STATE.BPS_ENABLED;
            }
            else if (!this.enabled)
            {
                pState[0].state = (enum_PENDING_BP_STATE)enum_BP_STATE.BPS_DISABLED;
            }

            return Constants.S_OK;
        }

        private void RemoveBreakpointInUDK()
        {
            switch ((enum_BP_LOCATION_TYPE)this.breakpointRequestInfo.bpLocation.bpLocationType)
            {
                case enum_BP_LOCATION_TYPE.BPLT_CODE_FILE_LINE:
                    string documentName;
                    TEXT_POSITION[] startPosition;
                    TEXT_POSITION[] endPosition;
                    GetStartPosition(out documentName, out startPosition, out endPosition);

                    this.engine.VSCommandSender.RemoveBreakpoint(
                        DocumentContext.GetClassNameFromFilePath(documentName),
                        startPosition[0].dwLine + 1);

                    break;
                case enum_BP_LOCATION_TYPE.BPLT_CODE_FUNC_OFFSET:

                    string funcName = GetFunctionPosition();
                    if (funcName == "breakonnone")
                    {
                        this.engine.VSCommandSender.RemoveBreakOnNone();
                    }
                    else
                    {
                        this.engine.VSCommandSender.RemoveDataWatch(funcName);
                    }

                    break;
                default:
                    return;
            }
        }

        private void SetBreakpointInUDK()
        {

            switch ((enum_BP_LOCATION_TYPE)this.breakpointRequestInfo.bpLocation.bpLocationType)
            {
                case enum_BP_LOCATION_TYPE.BPLT_CODE_FILE_LINE:
                    string documentName;
                    TEXT_POSITION[] startPosition;
                    TEXT_POSITION[] endPosition;
                    GetStartPosition(out documentName, out startPosition, out endPosition);

                    this.engine.VSCommandSender.SetBreakpoint(
                        DocumentContext.GetClassNameFromFilePath(documentName),
                        startPosition[0].dwLine + 1);

                    break;
                case enum_BP_LOCATION_TYPE.BPLT_CODE_FUNC_OFFSET:

                    string funcName = GetFunctionPosition();
                    if (funcName == "breakonnone")
                    {
                        this.engine.VSCommandSender.SetBreakOnNone();
                    }
                    else
                    {
                        this.engine.VSCommandSender.SetDataWatch(funcName);
                    }

                    break;
                default:
                    return;
            }

            this.engine.ExecSimulator.RaiseDebugEventsOnBreakpointBound(this);
        }

        private void GetStartPosition(out string documentName, out TEXT_POSITION[] startPosition, out TEXT_POSITION[] endPosition)
        {
            var docPosition =
                (IDebugDocumentPosition2)
                Marshal.GetObjectForIUnknown(this.breakpointRequestInfo.bpLocation.unionmember2);

            // Get the name of the document that the breakpoint was put in
            Core.CheckOk(docPosition.GetFileName(out documentName));

            // Get the location in the document that the breakpoint is in.
            startPosition = new TEXT_POSITION[1];
            endPosition = new TEXT_POSITION[1];
            Core.CheckOk(docPosition.GetRange(startPosition, endPosition));
        }

        private string GetFunctionPosition()
        {
            var funcPosition =
                (IDebugFunctionPosition2)
                Marshal.GetObjectForIUnknown(this.breakpointRequestInfo.bpLocation.unionmember2);

            string funcName = String.Empty;
            // Get the name of the document that the breakpoint was put in
            Core.CheckOk(funcPosition.GetFunctionName(out funcName));

            return funcName;
        }

        // The sample engine does not support conditions on breakpoints.
        int IDebugPendingBreakpoint2.SetCondition(BP_CONDITION bpCondition)
        {
            this.bpCondition = bpCondition;

            lock (this.boundBreakpointsLock)
            {
                foreach (var boundBreakpoint in this.boundBreakpoints)
                {
                    ((IDebugBoundBreakpoint2)boundBreakpoint).SetCondition(bpCondition);
                }
            }

            return Constants.S_OK;
        }

        // The sample engine does not support pass counts on breakpoints.
        int IDebugPendingBreakpoint2.SetPassCount(BP_PASSCOUNT bpPassCount)
        {
            this.bpPasscountStyle = bpPassCount;

            lock (this.boundBreakpointsLock)
            {
                foreach (var boundBreakpoint in this.boundBreakpoints)
                {
                    ((IDebugBoundBreakpoint2)boundBreakpoint).SetPassCount(bpPassCount);
                }
            }

            return Constants.S_OK;
        }

        // Toggles the virtualized state of this pending breakpoint. When a pending breakpoint is virtualized, 
        // the debug engine will attempt to bind it every time new code loads into the program.
        // The sample engine will does not support this.
        int IDebugPendingBreakpoint2.Virtualize(int fVirtualize)
        {
            Debug.WriteLine("IDebugPendingBreakpoint2 Virtualize");

            return Constants.S_OK;
        }

        #endregion
    }
}